{
  "nbformat": 4,
  "nbformat_minor": 0,
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "name": "python3",
      "display_name": "Python 3"
    },
    "language_info": {
      "name": "python"
    },
    "gpuClass": "standard"
  },
  "cells": [
    {
      "cell_type": "markdown",
      "source": [
        "# Classic Topic Modeling with BM25\n",
        "\n",
        "txtai 5.0 introduced topic modeling via [semantic graphs](https://neuml.hashnode.dev/introducing-the-semantic-graph). Semantic graphs can be easily integrated into an embeddings instance to add topic modeling to a txtai index.\n",
        "\n",
        "In addition to transformers-backed models, txtai also has support for traditional indexing methods. Given the modular design of txtai, traditional scoring methods like BM25 can be combined with graphs to build topic models. \n",
        "\n",
        "This notebook is all classic Python code on the CPU. No GPUs or machine learning models required!"
      ],
      "metadata": {
        "id": "-xU9P9iSR-Cy"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# Install dependencies\n",
        "\n",
        "Install `txtai` and all dependencies."
      ],
      "metadata": {
        "id": "shlUi2kKS7KT"
      }
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "xEvX9vCpn4E0"
      },
      "outputs": [],
      "source": [
        "%%capture\n",
        "!pip install git+https://github.com/neuml/txtai#egg=txtai[graph] datasets"
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# Load dataset\n",
        "\n",
        "This example will use the `ag_news` dataset, which is a collection of news article headlines."
      ],
      "metadata": {
        "id": "408IyXzKFSiG"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from datasets import load_dataset\n",
        "\n",
        "dataset = load_dataset(\"ag_news\", split=\"train\")"
      ],
      "metadata": {
        "id": "IQ_ns6YvFRm1"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "# Build BM25 Index\n",
        "\n",
        "Since the original txtai release, there has been a `scoring` package. This package supports building standalone BM25, TF-IDF and/or SIF text indexes."
      ],
      "metadata": {
        "id": "-vNVSA2FQnKj"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from txtai.scoring import ScoringFactory\n",
        "\n",
        "# List of all text elements\n",
        "texts = dataset[\"text\"]\n",
        "\n",
        "# Build index\n",
        "scoring = ScoringFactory.create({\"method\": \"bm25\", \"terms\": True})\n",
        "scoring.index((x, text, None) for x, text in enumerate(texts))\n",
        "\n",
        "# Show total\n",
        "scoring.count()"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "LJ2FskiiQ_l_",
        "outputId": "fc4685d9-5ec2-4fbe-a347-857a74b9b509"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "120000"
            ]
          },
          "metadata": {},
          "execution_count": 3
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "Let's test the index."
      ],
      "metadata": {
        "id": "BOW5JlxYS3Rm"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "for id, score in scoring.search(\"planets explore life earth\", 3):\n",
        "  print(id, texts[id], score)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "slLtmzfbRuf6",
        "outputId": "27aca9cb-8704-475c-c38d-01da65328686"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "16327 3 Planets Are Found Close in Size to Earth, Making Scientists Think 'Life' A trio of newly discovered worlds are much smaller than any other planets previously discovered outside of the solar system. 20.72295380862701\n",
            "16158 Earth #39;s  #39;big brothers #39; floating around stars Washington - A new class of planets has been found orbiting stars besides our sun, in a possible giant leap forward in the search for Earth-like planets that might harbour life. 19.917461045326878\n",
            "16620 New Planets could advance search for Life Astronomers in Europe and the United States have found two new planets about 20 times the size of Earth beyond the solar system. The discovery might be a giant leap forward in  19.917461045326878\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "Results look as expected. BM25 returns keyword-based results vs contextual matches."
      ],
      "metadata": {
        "id": "XnAhNb8Td6Wd"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# Build topic model\n",
        "\n",
        "Now that we have a scoring index, we'll use it to build a graph.\n",
        "\n",
        "Graphs have built-in methods to insert nodes and build a relationship index between the nodes. The `index` method takes a search parameter that can be any function that returns (id, score) pairs. This logic is built into embeddings instances. \n",
        "\n",
        "Graphs constructed via a BM25 index will have more literal relationships. In other words, it will be keyword-driven. Semantic graphs backed by embeddings will have contextual relationships.\n",
        "\n",
        "The next section builds a graph to support topic modeling. We'll use a multiprocessing pool to maximize CPU usage."
      ],
      "metadata": {
        "id": "gqEBeBEoTrup"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "import os\n",
        "\n",
        "from multiprocessing import Pool\n",
        "\n",
        "from txtai.graph import GraphFactory\n",
        "\n",
        "# Multiprocessing helper methods\n",
        "SCORING = None\n",
        "\n",
        "def create(search):\n",
        "    global SCORING\n",
        "\n",
        "    # Create a global scoring object\n",
        "    SCORING = search\n",
        "\n",
        "def run(params):\n",
        "    query, limit = params\n",
        "    return SCORING.search(query, limit)\n",
        "\n",
        "def batchsearch(queries, limit):\n",
        "    return pool.imap(run, [(query, limit) for query in queries])\n",
        "\n",
        "# Build the graph\n",
        "pool = None\n",
        "with Pool(os.cpu_count(), initializer=create, initargs=(scoring,)) as pool:\n",
        "    graph = GraphFactory.create({\"topics\": {}})\n",
        "    graph.insert((x, text, None) for x, text in enumerate(texts))\n",
        "    graph.index(batchsearch, None)"
      ],
      "metadata": {
        "id": "MEpZAr0TUMCK"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "Let's list the top 10 topics. Keep in mind this dataset is from 2004."
      ],
      "metadata": {
        "id": "aPaZsoxnYW8I"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "list(graph.topics)[:10]"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "HyKiVFBerQfw",
        "outputId": "fdba468b-bac1-4f63-c915-5fcad78880f7"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "['kerry_bush_john_president',\n",
              " 'nhl_players_league_lockout',\n",
              " 'arafat_yasser_palestinian_leader',\n",
              " 'sharon_ariel_prime_minister',\n",
              " 'blair_tony_minister_prime',\n",
              " 'xp_windows_microsoft_sp2',\n",
              " 'athens_gold_medal_olympic',\n",
              " 'space_prize_million_spaceshipone',\n",
              " 'nikkei_tokyo_reuters_average',\n",
              " 'hostage_british_bigley_iraq']"
            ]
          },
          "metadata": {},
          "execution_count": 10
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "Topics map a list of ids for each matching text element ordered by topic relevance. Let's print the most relevant text element for a topic."
      ],
      "metadata": {
        "id": "BKPJuq6br-ru"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "uid = graph.topics[\"xp_windows_microsoft_sp2\"][0]\n",
        "graph.attribute(uid, \"text\")"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 54
        },
        "id": "sYd9nKqYrwhe",
        "outputId": "0db63a74-2c01-433a-d754-6f0988894ffe"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "'Microsoft continues Windows XP SP2 distribution Continuing the roll-out of Windows XP Service Pack 2 (SP2), Microsoft Corp. on Wednesday began pushing the security-focused update to PCs running Windows XP Professional Edition '"
            ],
            "application/vnd.google.colaboratory.intrinsic+json": {
              "type": "string"
            }
          },
          "metadata": {},
          "execution_count": 44
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# Graph analysis\n",
        "\n",
        "Given this is a standard txtai graph, analysis methods such as centrality and pagerank are available."
      ],
      "metadata": {
        "id": "muN33RNB0Kob"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "centrality = list(graph.centrality().keys())\n",
        "print(\"Top connection count:\", [len(graph.edges(uid)) for uid in centrality[:5]], \"\\n\")\n",
        "\n",
        "# Print most central node/topic\n",
        "print(\"Most central node:\", graph.attribute(centrality[0], \"text\"))\n",
        "\n",
        "topic = graph.attribute(centrality[0], \"topic\")\n",
        "for uid in graph.topics[topic][:3]:\n",
        "  print(\"->\", graph.attribute(uid, \"text\"))"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "hpwfJFcRv19j",
        "outputId": "0bb4d53d-27c2-42f5-cfaf-89c785cb73da"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Top connection count: [30, 30, 28, 28, 28] \n",
            "\n",
            "Most central node: Manning Gets Chance to Start Giants Coach Tom Coughlin announced that rookie quarterback Eli Manning will start ahead of two-time M.V.P. Kurt Warner in Thursday's preseason game against Carolina.\n",
            "-> Manning Replaces Warner As Giants QB (AP) AP - Eli Manning has replaced Kurt Warner as the New York Giants' starting quarterback.\n",
            "-> Eli Manning replaces Warner at quarterback Eli Manning, the top pick in this year #39;s NFL draft, has been named the starting quarterback of the New York Giants. Coach Tom Coughlin made the announcement at a Monday news conference.\n",
            "-> Giants to Start Manning Against Carolina (AP) AP - Eli Manning is going to get a chance to open the season as the New York Giants' starting quarterback.\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "Notice the correlation between the number of connections and centrality.\n",
        "\n",
        "Given that BM25 is keyword-driven, we expect that the most central node would be text that is duplicative in nature. And that is the case here."
      ],
      "metadata": {
        "id": "lqOdJR6a0ftB"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# Walk the graph\n",
        "\n",
        "Just like semantic graphs, relationship paths can be explored."
      ],
      "metadata": {
        "id": "8u_tTNrq2EoN"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from IPython.display import HTML\n",
        "\n",
        "def showpath(source, target):\n",
        "  path = graph.showpath(source, target)\n",
        "  path = [graph.attribute(p, \"text\") for p in path]\n",
        "\n",
        "  sections = []\n",
        "  for x, p in enumerate(path):\n",
        "    # Print start node\n",
        "    sections.append(f\"{x + 1}. {p}\")\n",
        "\n",
        "  return HTML(\"<br/><br/>\".join(sections))"
      ],
      "metadata": {
        "id": "oodIUmwrsZRd"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "source": [
        "showpath(83978, 8107)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 208
        },
        "id": "gZzStey9tEkl",
        "outputId": "8e6398d5-0fd8-465c-ce95-45b98eb2c195"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "<IPython.core.display.HTML object>"
            ],
            "text/html": [
              "1. NFL Game Summary - NY Jets at Buffalo Orchard Park, NY (Sports Network) - Willis McGahee ran for 132 yards and a touchdown to lead the Buffalo Bills to a 22-17 victory over the New York Jets at Ralph Wilson Stadium.<br/><br/>2. NCAA Game Summary - Marshall at Georgia Athens, GA (Sports Network) - Michael Cooper ran for the only touchdown of the game, as third-ranked Georgia rode its defense to a 13-3 victory over Marshall at Sanford Stadium.<br/><br/>3. NCAA Game Summary - Northwestern at Wisconsin Madison, WI (Sports Network) - Anthony Davis ran for 122 yards and two touchdowns to lead No. 6 Wisconsin over Northwestern, 24-12, to celebrate Homecoming weekend at Camp Randall Stadium.<br/><br/>4. NCAA Top 25 Game Summary - Northwestern at Minnesota The last time Minnesota won four games to start three consecutive seasons was 1934-36...Chris Malleo replaced Basanez for two series in the third quarter for his first career appearance.<br/><br/>5. UConn ousts Marist Sophomore Steve Sealy netted his third winning goal in the last four games, giving Connecticut a 2-1 overtime victory over Marist yesterday in an NCAA Division 1 first-round men's soccer playoff game at Morrone Stadium in Storrs, Conn.<br/><br/>6. United States upsets Germany to move to soccer semifinals Deep into overtime, and maybe the last time for the Fab Five of US women #39;s soccer, the breaks were going against them. A last-gasp goal that stole victory in regulation, a wide-open shot that bounced off the goal post."
            ]
          },
          "metadata": {},
          "execution_count": 43
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "Notice how the data pivots from the start node to the end node. If you've read the [Introducing the Semantic Graph](https://neuml.hashnode.dev/introducing-the-semantic-graph) article, you'll notice how this traversal is more literal in nature. In other words, the relationships are keyword-driven vs contextual."
      ],
      "metadata": {
        "id": "5qv1CbY35uUy"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# Wrapping up\n",
        "\n",
        "This notebook demonstrated how graphs can index traditional indexes such as BM25. This method can also be applied to an external index provided a search function is available to build connections.\n",
        "\n",
        "Semantic graphs backed by embeddings instances have a number of advantages and are recommended in most cases. But this is a classic way to do it - no machine learning models required!"
      ],
      "metadata": {
        "id": "4L8smyyXc8q8"
      }
    }
  ]
}